# -*- coding:utf-8 -*-
import csv
import json
import os.path
import string
import random
import traceback
import zipfile
import base64
from obs import ObsClient

# 函数本次挂载目录，默认情况下会为函数的 /tmp 目录分配 512 MB 的空间。
LOCAL_MOUNT_PATH = '/tmp/'

# 车辆数据转csv配置，key为车辆数据类型，value为对应csv文件名称，字段名和排序关键字配置。
CAR_CSV_CONFIG_DICT = {
    "6": {"csv_name": "can数据大气压力.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "4": {"csv_name": "can数据发动机油温.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "11": {"csv_name": "can数据发动机运行时间.csv",
           "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
           "sort_key": "时间"},
    "5": {"csv_name": "can数据机油压力.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "7": {"csv_name": "can数据进气温度.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "9": {"csv_name": "can数据冷却剂温度.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "2": {"csv_name": "can数据扭矩.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "10": {"csv_name": "can数据燃油累积使用量.csv",
           "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
           "sort_key": "时间"},
    "13": {"csv_name": "can数据瞬时油耗.csv",
           "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
           "sort_key": "时间"},
    "33": {"csv_name": "can数据整车负荷率.csv",
           "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
           "sort_key": "时间"},
    "1": {"csv_name": "can数据转速.csv",
          "fieldnames": ['vin', '类型', '时间', '保存时间', '原始值', '转换值'],
          "sort_key": "时间"},
    "纬度": {"csv_name": "gps数据.csv",
           "fieldnames": ['vin', '时间', '保存时间', '高度', '经度', '纬度', '速度(公里/时)',
                          '里程(公里)',
                          '方向', '定位标记', '南北纬标记', '东西经标记'], "sort_key": "时间"},
    "T15开关": {"csv_name": "开关量数据.csv",
              "fieldnames": ['vin', '时间', '保存时间', '原始值', '油门踏板', '刹车', '手刹',
                             '离合器状态', '倒车',
                             '举升缸状态', '排气制动开关', '翻斗未回位', '驾驶室翻转', '左转向灯',
                             '右转向灯',
                             '远光灯', '取力器开关', '取力器空档', '空滤报警', '空调', '加热器',
                             '近光灯', '空档', '制动蹄片磨损信号', 'T15开关'],
              "sort_key": "时间"},
    "0": {"csv_name": "扩展数据ACC.csv",
          "fieldnames": ['vin', '类型', '时间', '数据保存时间', '值1', '值2', '值3', '值4'],
          "sort_key": "时间"},
    "28": {"csv_name": "扩展数据锁车状态.csv",
           "fieldnames": ['vin', '类型', '时间', '数据保存时间', '值1', '值2', '值3', '值4'],
           "sort_key": "时间"},
    "脉冲车速": {"csv_name": "脉冲数据.csv",
             "fieldnames": ['vin', '时间', '数据保存时间', '里程', '脉冲车速', '转速'],
             "sort_key": "时间"},
    "油量数据": {"csv_name": "油量数据.csv",
             "fieldnames": ['vin', '类型', '时间', '数据保存时间', '值'], "sort_key": "时间"}
}


def handler(event, context):
    """method handler.

        函数执行入口，根据收到的APIG的请求事件，将指定车架号和时间的原始车辆数据转换为csv格式，
        然后达成zip包，写入到obs桶下的/csv_data/${taskID}-${vin}.zip对象中

        参数:
            event: APIG 触发器事件，例子如下：
                {
                    "body": {
                        "taskID": "002",
                        "vinList": [
                            "DM666666"
                        ],
                        "date": "2023-05-09",
                        "hours": [
                            "17"
                        ]
                    },
                    "httpMethod": "POST",
                    "pathParameters": {},
                    "headers": {
                        "accept-language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
                        "accept-encoding": "gzip, deflate, br"
                    "path": "/EMQ-parse-data",
                    "isBase64Encoded": false
                }
            context: Runtime提供的函数执行上下文

        返回: HTTP响应，200 成功，500 失败

    """
    log = context.getLogger()
    emq_data_parse_handler = EMQDataParseHandler(context)
    try:
        emq_data_parse_handler.run(event['body'])
        return response(200, "success")
    except:
        exec_info = traceback.format_exc()
        log.error(f"failed to run emq data handler: {exec_info}")
        return response(500, f"failed to run emq data handler: {exec_info}")
    finally:
        emq_data_parse_handler.obs_client.close()


def response(status_code, result):
    return {
        "statusCode": status_code,
        "isBase64Encoded": False,
        "body": result,
        "headers": {
            "Content-Type": "application/json"
        }
    }


def write_csv(csv_path, fieldnames, rows):
    """method write_csv.

        将 rows 数据按 fieldnames 写入指定 csv_path 的csv文件中

        参数:
            csv_path: csv文件路径
            fieldnames: 字段列表
            rows: 车辆数据

        返回:
            无
    """
    exists = os.path.exists(csv_path)
    with open(csv_path, 'a', newline='') as csv_file:
        writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
        if not exists:
            writer.writeheader()
        writer.writerows(rows)


def read_data(download_path):
    """method read_data.

        读取download_path的文件内容

        参数:
            download_path: 下降文件路径

        返回:
            文件内容
    """
    with open(download_path, 'r') as f:
        return f.readlines()


def take_sort_value(elem):
    return elem["时间"]


class EMQDataParseHandler:
    """EMQ传输到KAFKA的车辆数据处理类

        解析EMQ传输到KAFKA的车辆数据，按车架号和实际归类存储到obs

        属性:
            logger: 日志记录器
            obs_client: obs客户端
            download_dir: 下载目录
            original_data_bucket: 车辆原始数据存储桶.
            original_data_prefix: 车辆原始数据前缀路径.
            parse_data_prefix: 转csv并达成zip包的存储前缀路径.
    """

    def __init__(self, context):
        self.logger = context.getLogger()
        obs_endpoint = context.getUserData("obs_endpoint")
        self.obs_client = new_obs_client(context, obs_endpoint)
        self.download_dir = gen_local_download_path()
        self.original_data_bucket = context.getUserData("original_data_bucket")
        self.original_data_prefix = context.getUserData("original_data_prefix", "")
        self.parse_data_prefix = context.getUserData("parse_data_prefix", "")

    def run(self, body):
        """method run.

            根据请求体将指定车架号和时间的原始车辆数据转换为csv格式，然后达成zip包，上传到obs

            参数:
                body:
                    taskID: 为任务ID, 即本次转换的任务ID
                    vinList: 为本次任务指定转换的车架号列表，例如把DM666666车架号的原始数据转换为csv
                    date: 为本次任务指定转换日期的车辆数据，例如把DM666666车架号2023-03-01原始数据转换为csv
                    hours: 为本次任务指定日期下更细精度的小时数，不传则解析全天的原始数据，传把指定小时数的原始数据转换为csv，
                    例如把DM666666车架号2023-03-01 01-06小时的原始数据转换为csv

            返回:
                无
        """
        body = json.loads(base64.b64decode(body))
        vin_list = body['vinList']
        date = body['date']
        task_id = body['taskID']
        for vin in vin_list:
            if "hours" not in body:
                # 查询全天数据
                prefix_key = self.get_vin_data_prefix_key(vin, date)
                keys = self.get_vin_data_keys(prefix_key)
            else:
                keys = self.get_vin_data_keys_by_hours(vin, date, body["hours"])
            self.logger.info('started to write data to csv')
            self.write_keys_to_csv(keys)
            self.logger.info('succeed to write data to csv')
            # 打成压缩包上传
            output_zip_file = os.path.join(self.download_dir, vin + ".zip")
            with zipfile.ZipFile(output_zip_file, 'w',
                                 zipfile.ZIP_DEFLATED) as z_file:
                files = os.listdir(os.path.join(self.download_dir, "csv"))
                for filename in files:
                    csv_path = os.path.join(self.download_dir, "csv", filename)
                    z_file.write(csv_path, filename)
            output_object_key = self.parse_data_prefix + "/" + \
                                task_id + "-" + vin + ".zip"
            self.upload_file_to_obs(output_object_key, output_zip_file)

    def get_vin_data_keys(self, prefix_key):
        """method get_vin_data_keys.

            获取前缀匹配的对象名称数组

            参数:
                prefix_key: 前缀路径

            返回:
                前缀匹配的对象名称数组
            抛出:
                Exception: 列举对象失败的请求id和错误码
        """
        resp = self.obs_client.listObjects(self.original_data_bucket, prefix=prefix_key, max_keys=100)
        if resp.status < 300:
            keys = []
            for content in resp.body.contents:
                if content.key == prefix_key:
                    continue
                keys.append(content.key)
            return keys
        else:
            error_info = f'failed to listObjects {prefix_key}, ' \
                         f'requestId: {resp.requestId}, ' \
                         f'errorCode: {resp.errorCode}'
            raise Exception(error_info)

    def get_vin_data_prefix_key(self, vin, date):
        return self.original_data_prefix + "/" + vin + "/" + date

    def download_from_obs(self, object_key):
        """method download_from_obs.

            下载指定对象到内存

            参数:
                object_key: 指定对象名称

            返回:
                对象内容
            抛出:
                Exception: 下载对象失败的请求id和错误码
        """
        resp = self.obs_client. \
            getObject(self.original_data_bucket, object_key, loadStreamInMemory=True)
        if resp.status < 300:
            return resp.body.buffer
        else:
            error_info = f'failed to download_from_obs {object_key}, ' \
                         f'requestId: {resp.requestId}, ' \
                         f'errorCode: {resp.errorCode}'
            raise Exception(error_info)

    def upload_file_to_obs(self, object_key, local_file):
        """method upload_file_to_obs.

            将本地文件上传到obs

            参数:
                object_key: 对象名称
                local_file: 本地文件路径

            返回:
                无
            抛出:
                Exception: 上传对象失败的请求id和错误码
        """
        resp = self.obs_client.putFile(self.original_data_bucket,
                                       object_key, local_file)
        if resp.status > 300:
            error_info = f'failed to upload_file_to_obs {object_key}, ' \
                         f'requestId: {resp.requestId}, ' \
                         f'errorCode: {resp.errorCode}'
            raise Exception(error_info)

    def parse_lines_to_dict(self, lines):
        """method download_from_obs.

            解析lines中的每行数据 根据数据类型进行归类

            参数:
                lines: 多行车辆数据

            返回:
                归类后的字典
        """
        data_dict = {}
        for line in lines:
            if line is None or line == "":
                continue
            d = json.loads(line)
            if '类型' in d:
                type_value = d['类型']
                if (type_value == '11' or type_value == '2') and '数据保存时间' in d:
                    type_value = '油量数据'

                if type_value in data_dict:
                    data_list = data_dict[type_value]
                    data_list.append(d)
                    data_dict[type_value] = data_list
                else:
                    data_list = [d]
                    data_dict[type_value] = data_list
            elif '纬度' in d:
                if '纬度' in data_dict:
                    data_list = data_dict['纬度']
                    data_list.append(d)
                    data_dict['纬度'] = data_list
                else:
                    data_list = [d]
                    data_dict['纬度'] = data_list
            elif 'T15开关' in d:
                if 'T15开关' in data_dict:
                    data_list = data_dict['T15开关']
                    data_list.append(d)
                    data_dict['T15开关'] = data_list
                else:
                    data_list = [d]
                    data_dict['T15开关'] = data_list
            elif '脉冲车速' in d:
                if '脉冲车速' in data_dict:
                    data_list = data_dict['脉冲车速']
                    data_list.append(d)
                    data_dict['脉冲车速'] = data_list
                else:
                    data_list = [d]
                    data_dict['脉冲车速'] = data_list
            else:
                self.logger.waring('line %s is not processed.', line)

        return data_dict

    def get_vin_data_keys_by_hours(self, vin, date, specified_hours):
        """method get_vin_date_keys_by_hours.

            根据车架号、日期和指定小时数生成要从obs提取的车辆原始数据对象名称数组

            参数:
                vin: 车架号
                date: 日期 年-月-日
                specified_hours: 指定小时数

            返回:
                需从obs提取的车辆原始数据对象名称数组
        """
        keys = []
        for hour in specified_hours:
            keys.append(self.original_data_prefix + "/" + vin +
                        "/" + date + "|" + hour + ".txt")
        return keys

    def write_keys_to_csv(self, keys):
        """method write_keys_to_csv.

            从obs下载指定keys的车辆原始数据，转换成csv

            参数:
                keys: 需从obs提取的车辆原始数据对象名称数组

            返回:
                无
        """
        os.makedirs(os.path.join(self.download_dir, "csv"))
        for object_key in keys:
            data = self.download_from_obs(object_key)
            lines = str(data, encoding="utf-8").split("\n")
            dict_data = self.parse_lines_to_dict(lines)
            for key in dict_data:
                filename = CAR_CSV_CONFIG_DICT[key]['csv_name']
                fieldnames = CAR_CSV_CONFIG_DICT[key]['fieldnames']
                csv_path = os.path.join(self.download_dir, "csv", filename)
                data_list = dict_data[key]
                data_list.sort(key=take_sort_value, reverse=True)
                write_csv(csv_path, fieldnames, dict_data[key])


def new_obs_client(context, obs_server):
    """method new_obs_client.

        创建一个obs客户端.

        参数:
            context: Runtime提供的函数执行上下文，获取临时ak sk, 需要配置授权了OBS Admin权限的委托
            obs_server: OBS的endpoint，例如 obs.cn-east-3.myhuaweicloud.com

        返回:
            obs客户端.
    """
    ak = context.getAccessKey()
    sk = context.getSecretKey()
    return ObsClient(access_key_id=ak, secret_access_key=sk, server=obs_server)


def gen_local_download_path():
    """method gen_local_download_path.

        生成一个本地下载路径.

        参数:
            无

        返回:
            本地下载路径.
    """
    letters = string.ascii_letters
    download_dir = LOCAL_MOUNT_PATH + ''.join(
        random.choice(letters) for i in range(16)) + '/'
    os.makedirs(download_dir)
    return download_dir
